package com.fouram.web.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ReflectUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelGenerateException;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.FileUtils;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fouram.core.constants.GlobalConstants;
import com.fouram.core.domain.R;
import com.fouram.core.base.BasePageBo;
import com.fouram.core.base.ExportExcelInfo;
import com.fouram.core.base.ImportExcelInfo;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * easy-excel导入导出工具类
 * @author liupeng
 */
@Slf4j
public class ExcelUtils {

    /**
     * @Description 通用的配置
     * @date 10:33 2023/12/22
     * @param response
     * @param fileName
     * @return com.alibaba.excel.write.style.HorizontalCellStyleStrategy
     **/
    @SneakyThrows
    public static HorizontalCellStyleStrategy getCellStyleStrategy(HttpServletResponse response, String fileName) {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        //设置表头居中对齐
        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        //内容样式
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //设置内容靠左对齐
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
        return new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
    }

    /**
     * 将列表以 Excel 响应给前端
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表哦
     * @param <T>       泛型，保证 head 和 data 类型的一致性
     * @throws IOException 写入失败的情况
     */
    public static <T> void write(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {
        // 输出 Excel
        EasyExcel.write(response.getOutputStream(), head)
                .autoCloseStream(false) // 不要自动关闭，交给 Servlet 自己处理
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度，自动适配。最大 255 宽度
                // .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
                .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
                .sheet(sheetName).doWrite(data);
        // 设置 header 和 contentType。写在最后的原因是，避免报错时，响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }

    /**
     * 导出excel，支持多sheet和是否分页
     * @param response
     * @param fileName
     * @param exportExcelInfos
     * @param <T>
     */
    @SneakyThrows
    public static <T extends BasePageBo> void doExport(HttpServletResponse response, String fileName, ExportExcelInfo<T>... exportExcelInfos) {
        try(ServletOutputStream out = response.getOutputStream()) {
            ExcelWriter excelWriter = EasyExcelFactory.write(out).autoCloseStream(true)
                    .registerWriteHandler(getCellStyleStrategy(response, fileName)).build();
            for (int i = 0; i < exportExcelInfos.length; i++) {
                ExportExcelInfo exportExcelInfo = exportExcelInfos[i];
                BasePageBo req = exportExcelInfo.getSearchParam();
                if (StringUtils.isBlank(exportExcelInfo.getSheetName())) {
                    exportExcelInfo.setSheetName(fileName);
                }
                WriteSheet writeSheet  = EasyExcelFactory.writerSheet(i, exportExcelInfo.getSheetName()).
                        head(exportExcelInfo.getHeadClass()).build();
                if (exportExcelInfo.getPageSearch() != null && exportExcelInfo.getPageSearch()) {
                    int page = 0;
                    while (true) {
                        req.setPageNo(++page);
                        req.setPageSize(GlobalConstants.EXPORT_PAGE_SIZE_DEFAULT);
                        List voList = (List) exportExcelInfo.getListFunction().apply(req);
                        excelWriter.write(voList, writeSheet);
                        if (CollectionUtils.isEmpty(voList) || voList.size() < GlobalConstants.EXPORT_PAGE_SIZE_DEFAULT) {
                            break;
                        }
                        voList.clear();
                    }
                } else{
                    List voList = (List) exportExcelInfo.getListFunction().apply(req);
                    excelWriter.write(voList, writeSheet);
                }
            }
            excelWriter.finish();
            out.flush();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * 读取excel数据
     * @param file
     * @param head
     * @param <T>
     * @return
     * @throws IOException
     */
    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        return EasyExcel.read(file.getInputStream(), head, null)
                .autoCloseStream(false)  // 不要自动关闭，交给 Servlet 自己处理
                .doReadAllSync();
    }

    /**
     * 导入excel，错误信息返回excel文件，根据contentType区分导入是否成功
     * application/json; charset=utf-8 导入全部成功 否则 导入有错误返回错误数据excel
     * @param response
     * @param importExcelInfos
     * @return
     */
    @SneakyThrows
    public static <VO> void doImport(HttpServletResponse response,MultipartFile file,ImportExcelInfo<VO>... importExcelInfos) {
        File tempXlsx = null;
        try {
            tempXlsx = FileUtils.createTmpFile(IdUtil.fastSimpleUUID() + ".xlsx");
            // 文件名称
            String errorFileName = FileNameUtil.mainName(file.getOriginalFilename()) + "（错误数据）";
            ExcelWriter excelWriter = EasyExcelFactory.write(tempXlsx).autoCloseStream(true)
                    .registerWriteHandler(getCellStyleStrategy(response, errorFileName)).build();
            List<String> sheetNameList = Lists.newArrayList();
            AtomicInteger errorCount = new AtomicInteger(0);
            for (int i = 0; i < importExcelInfos.length; i++) {
                ImportExcelInfo<VO> info = importExcelInfos[i];
                final List<List<Object>> errorList = Lists.newArrayList();
                final List<List<String>> headList = Lists.newArrayList();
                final List<WriteSheet> writeSheetList = Lists.newArrayList();
                int finalI = i;
                ReadListener<VO> readListener = new AnalysisEventListener<VO>() {
                    @SneakyThrows
                    @Override
                    public void invoke(VO vo, AnalysisContext analysisContext) {
                        // 只获取一次
                        if (CollUtil.isEmpty(headList)) {
                            // 获取sheet名称
                            sheetNameList.add(analysisContext.readSheetHolder().getSheetName());
                            // 获取excel文件head
                            Map<Integer, Head> headMap = analysisContext.currentReadHolder().excelReadHeadProperty().getHeadMap();
                            if (MapUtil.isNotEmpty(headMap)) {
                                headMap.forEach((key, value) -> headList.add(value.getHeadNameList()));
                                // 最后一列添加错误信息
                                headList.add(Lists.newArrayList("错误信息"));
                            }
                            // 初始化输出错误数据对应的sheet
                            writeSheetList.add(EasyExcelFactory.writerSheet(finalI, sheetNameList.get(finalI)).head(headList).build());
                        }
                        try {
                            // 持久化数据
                            info.getPersistenceConsumer().accept(vo);
                        } catch (Exception e) {
                            log.error(e.getMessage(), e);
                            // 通过反射获取原vo对应所有属性值
                            List<Object> row = Lists.newArrayList();
                            for (Field field : ReflectUtil.getFields(info.getHeadClass())) {
                                field.setAccessible(true);
                                row.add(field.get(vo));
                            }
                            // 返回错误信息
                            row.add(e.getMessage());
                            errorList.add(row);
                            errorCount.getAndAdd(1);
                        }
                    }
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("excel数据解析完成");
                    }
                };
                // 读取excel
                EasyExcel.read(file.getInputStream(),info.getHeadClass(),readListener).autoCloseStream(true).sheet(i).doRead();
                excelWriter.write(errorList, writeSheetList.get(0));
            }
            excelWriter.finish();
            // 导入成功，无异常数据，直接返回json
            if (errorCount.get() == 0) {
                response.setContentType("application/json; charset=utf-8");
                response.setCharacterEncoding("utf-8");
                try (PrintWriter writer = response.getWriter()) {
                    writer.write(JSON.toJSONString(R.ok("导入成功")));
                    writer.flush();
                }
            } else {
                FileUtil.writeToStream(tempXlsx, response.getOutputStream());
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            if (tempXlsx != null && !tempXlsx.delete()) {
                throw new ExcelGenerateException("Can not delete temp File!");
            }
        }
    }

    /**
     * 获取excel列头
     * @param file
     * @return
     */
    @SneakyThrows
    public static Map<Integer, String> getHeader(MultipartFile file, int sheetNo) {
        final Map<Integer, String> cachedFieldMap = Maps.newLinkedHashMap();
        ReadListener readListener = new AnalysisEventListener() {
            @Override
            public void invoke(Object vo, AnalysisContext analysisContext) {
                // 只获取一次表头
                if (MapUtil.isEmpty(cachedFieldMap)) {
                    Map<Integer, Head> headMap = analysisContext.currentReadHolder().excelReadHeadProperty().getHeadMap();
                    if (MapUtil.isNotEmpty(headMap)) {
                        headMap.forEach((index, head) -> cachedFieldMap.put(index, head.getFieldName()));
                    }
                    return;
                }
            }
            @Override
            public void doAfterAllAnalysed(AnalysisContext analysisContext) {
            }
        };
        EasyExcel.read(file.getInputStream(), readListener).autoCloseStream(true).sheet(sheetNo).doRead();
        return cachedFieldMap;
    }
}
